package com.lagou.edu.factory;

import com.lagou.edu.annotation.Autowired;
import com.lagou.edu.annotation.Service;
import com.lagou.edu.annotation.Transactional;
import com.lagou.edu.utils.BeanFactoryUtils;
import com.lagou.edu.utils.TransactionManager;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.net.URLDecoder;
import java.sql.SQLException;
import java.util.*;

public class AnnotationBeanFactory {

    private static final String FILE_PROTOCOL = "file";

    private Map<String, Object> map = new HashMap<>();
    private Set<Class> annotatedBeans = new HashSet<>();

    public void loadBeans(String... packages) {
        if (packages.length > 0) {
            for (String aPackage : packages) {
                String packagePathName = aPackage.replace(".", "/");
                Enumeration<URL> resources = null;
                try {
                    resources = Thread.currentThread().getContextClassLoader().getResources(packagePathName);
                    while (resources.hasMoreElements()) {
                        URL url = resources.nextElement();
                        String protocol = url.getProtocol();
                        if (FILE_PROTOCOL.equals(protocol)) {
                            String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                            // 扫描有Service注解的类，将类名存到annotatedBeans
                            scanClass(aPackage, filePath);
                        }
                    }
                    initialInstance();
                    initialTransaction();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }

        }

    }

    private void initialTransaction() {
        for (String key : map.keySet()) {
            Object object = map.get(key);
            Class<?> aClass = object.getClass();
            Method[] methods = aClass.getMethods();
            for (Method method : methods) {
                Transactional transactional = method.getAnnotation(Transactional.class);
                if (transactional != null) {
                    Class<?>[] interfaces = aClass.getInterfaces();
                    Object proxy = null;
                    if (interfaces.length > 0) {
                        proxy = getJdkProxy(aClass, object);
                    } else {
                        proxy = getCglibProxy(aClass, object);
                    }

                    map.put(key, proxy);
                }
            }
        }
    }

    private Object getJdkProxy(Class clazz, Object object) {
        return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), (p, m, args) -> {
            return doInvoke(m, object, args);
        });
    }

    private Object getCglibProxy(Class clazz, Object object) {
        return Enhancer.create(clazz, (MethodInterceptor)(o, method, objects, methodProxy) -> {
            return doInvoke(method, object, objects);
        });
    }

    private Object doInvoke(Method method, Object object, Object[] objects)
        throws SQLException, InvocationTargetException, IllegalAccessException {
        Object result = null;
        TransactionManager transactionManager = (TransactionManager)map.get("transactionManager");
        try {
            // 开启事务(关闭事务的自动提交)
            transactionManager.beginTransaction();

            result = method.invoke(object, objects);

            // 提交事务

            transactionManager.commit();
        } catch (Exception e) {
            e.printStackTrace();
            // 回滚事务
            transactionManager.rollback();

            // 抛出异常便于上层servlet捕获
            throw e;

        }
        return result;
    }

    // 创建对象，保存到map中
    private void initialInstance()
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 遍历annotatedBeans
        if (annotatedBeans.size() > 0) {
            for (Class annotatedBean : annotatedBeans) {
                createInstance(annotatedBean);
            }
        }
    }

    private Object createInstance(Class annotatedBean)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Service annotation = (Service)annotatedBean.getAnnotation(Service.class);
        String defaultName = annotation.value();
        String className = annotatedBean.getSimpleName();
        String id = (defaultName != null && defaultName.length() > 0) ? defaultName
            : className.substring(0, 1).toLowerCase() + className.substring(1);
        // 判断是否已存在map中
        if (!map.containsKey(id)) {
            // 开始初始化
            Object object = annotatedBean.getDeclaredConstructor().newInstance();
            populateInstance(annotatedBean, object);
            map.put(id, object);
            return object;
        }
        return null;
    }

    // 填充属性
    private void populateInstance(Class clazz, Object object)
        throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            Autowired autowired = field.getAnnotation(Autowired.class);
            if (autowired != null) {
                Class<?> fieldType = field.getType();
                Object fieldObject = getMap(fieldType);
                if (fieldObject == null) {
                    Class annotatedBean = getAnnotatedBeans(fieldType);
                    if (annotatedBean != null) {
                        Object fieldInstance = createInstance(annotatedBean);
                        if (fieldInstance != null) {
                            fieldObject = fieldInstance;
                        }
                    }
                }
                field.setAccessible(true);
                field.set(object, fieldObject);
            }

        }
    }

    private Class getAnnotatedBeans(Class target) {
        for (Class annotatedBean : annotatedBeans) {
            if (target.isAssignableFrom(annotatedBean)) {
                return annotatedBean;
            }
        }
        return null;
    }

    private Object getMap(Class<?> fieldType) {
        Object object = null;
        if (map.size() > 0) {
            for (String key : map.keySet()) {
                object = map.get(key);
                if (fieldType.isInstance(object)) {
                    return object;
                }
            }
        }

        return object;
    }

    // 扫描有Service注解的类
    private void scanClass(String packageName, String filePath) throws ClassNotFoundException {
        File dir = new File(filePath);
        if (dir.exists() && dir.isDirectory()) {
            File[] files = dir.listFiles(file -> {
                return file.isDirectory() || file.getName().endsWith(".class");
            });
            if (files != null && files.length > 0) {
                for (File file : files) {
                    if (file.isDirectory()) {
                        scanClass(packageName + "." + file.getName(), file.getAbsolutePath());
                    } else {
                        String className = file.getName().substring(0, file.getName().length() - 6);
                        Class<?> aClass = Class.forName(packageName + "." + className);
                        Service annotation = aClass.getAnnotation(Service.class);
                        if (annotation != null) {
                            annotatedBeans.add(aClass);
                        }

                    }
                }
            }
        }

    }

    public Object getBean(String id) {
        return map.get(id);
    }
}
